개요
안녕하세요! 오늘은 파이썬의 discord.py
를 사용하여 자신만의 디스코드 봇을 만드는 방법을 상세하게 알아보려고 합니다!
시작하기 전에
1. 파이썬 설치
파이썬 3.8 이상 버전이 설치되어 있어야 합니다.
2. discord.py
설치
터미널 또는 명령 프롬프트에서 다음 명령어를 실행하여 라이브러리를 설치해주세요.
pip install -U discord.py
# 또는 interactions, ui 등을 모두 사용하려면
# pip install -U discord.py[voice] # 음성 기능 포함 시
3. 디스코드 봇 생성 및 토큰 발급
디스코드 봇을 원활하게 활성화하기 위해서는 몇 가지의 사전 준비 작업이 필요합니다.
아래 단계들을 차근차근 따라하여 사전 준비를 완료해봐요!
3-1. 앱(봇) 생성
우선 디스코드 개발자 포털에 들어가서 새로운 애플리케이션(Bot)을 생성해주도록 하자.
(디스코드 가입이 되어있지 않다면 가입 먼저)
3-2. 토큰 발급
애플리케이션을 생성하였다면 해당 애플리케이션의 토큰을 발급 받아야한다.
아래의 사진과 같이 해당 앱의 설정 - 봇 메뉴에서 'Reset Token'을 실행하여 토큰을 얻도록 하자.
💥 해당 토큰은 절대로 외부에 공유하지 않도록 하고, 노출되었다면 'Reset Token'으로 재갱신 해주도록 하자.
3-3. 권한 부여
Bot이 메시지 콘텐츠를 수신하기 위해 권한 설정이 필요하다.
Privileged Gateway Intents 영역에 MESSAGE CONTENT INTENT를 활성화하고 [Save Changes]를 클릭하자.
Discord Privileged Gateway Intents는 Bot과 Application이 특정 민감한 데이터 및 기능에 액세스 할 수 있도록 허용하는 권한 시스템이다.
Gateway Intents 시스템은 2018년 Discord에서 도입했으며 Bot과 Application에서 명시적으로 요청하지 않는 한 특정 데이터 및 이벤트에 대한 액세스를 제한하여 서버 성능을 개선하고 사용자 개인 정보를 보호하도록 설계되었다.
Gateway Intent에는 "privileged"과 "unprivileged" 두 가지 유형이 존재한다.
privileged에는 서버 구성원 존재 및 구성원 데이터가 포함되며 unprivileged에는 비구성원 데이터 및 이벤트가 포함된다.
기본적으로 Discord Bot은 privileged에 액세스 할 수 없으며 Discord 개발자 포털을 통해 명시적으로 액세스를 요청해야 가능하다.
discord.errors.PrivilegedIntentsRequired: Shard ID None is requesting privileged intents that have not been explicitly enabled in the developer portal. It is recommended to go to https://discord.com/developers/applications/ and explicitly enable the privileged intents within your application's page. If this is not possible, then consider disabling the privileged intents instead.
만약 위와 같은 오류 메시지가 나왔다면 권한 설정이 필요하므로, 위 과정을 실시해주도록 하자.
3-4. 봇 초대하기
방금 생성한 봇을 내 채널에 초대해보자.
초대할 앱의 설정 - OAuth2 - URL Generator 메뉴에서 'bot'을 선택하자.
개발할 bot에 필요한 기능들을 체크하고, 화면 하단에 보이는 GENERATED URL을 복사하여 브라우저에서 실행해주자. 그러면 봇을 초대할 디스코드 서버를 선택할 수 있다.
연결이 완료되었다.
목차
- 1. 기본적인 Discord.py 봇 구조
- 1-1. 봇 테스트
- 2. Cog 모듈화 방법
- 3. 더 다양한 기능 구현해보기
- 3-1. Slash Command (슬래시 명령어)
- 3-1-1. 테스트
- 3-2. Embed 메시지
- 3-2-1. 테스트
- 3-3. 반복 작업 처리 : task
- 3-3-1. 테스트
- 3-4. 봇 상태 처리: Activity Type
- 3-4-1. 테스트
- 3-5. 생성형 버튼
- 3-5-1. 테스트
- 3-6. 사용자 입력: Form (Modal)
- 3-6-1. 테스트
- 3-7. 자동 삭제 메시지 (delete_after)
- 3-7-1. 테스트
- 3-8. 채널 메시지 삭제하는 방법
- 3-8-1. 테스트
- 3-9. 응답 지연 해결 방법 - Interaction.response.defer()
- 3-1. Slash Command (슬래시 명령어)
- 4. 그 외 자주 쓰이는 기능들
- 오류 처리 (Error Handling)
- 권한 확인 (Permission Checks)
- 객체 가져오기 (Getting Objects)
- 이벤트 핸들링 (Event Handling)
1. 기본적인 Discord 봇 구조
discord.py
공식 문서
가장 기본적인 형태의 디스코드 봇 코드입니다.
이 코드는 봇이 성공적으로 온라인 상태가 되면 콘솔에 메시지를 출력하고, 사용자가 "안녕"이라고 메시지를 보내면 "안녕하세요!"라고 응답합니다.
# main.py
import discord
from discord.ext import commands
# 봇이 어떤 활동을 할지 '인텐트(intent)'를 설정합니다.
# message_content 인텐트가 활성화되어야 메시지 내용을 읽을 수 있습니다.
intents = discord.Intents.default()
intents.message_content = True # 메시지 내용을 읽기 위한 인텐트 활성화
# commands.Bot 객체를 생성합니다. 명령어 접두사는 '!'로 설정합니다.
# help_command=None으로 설정하면 기본 제공되는 help 명령어를 비활성화합니다.
bot = commands.Bot(command_prefix='/', intents=intents, help_command=None)
# 봇이 준비되었을 때 실행될 이벤트 핸들러입니다.
@bot.event
async def on_ready():
print(f'{bot.user.name}이(가) 성공적으로 로그인했습니다!')
print(f'봇 ID: {bot.user.id}')
print('------')
# 메시지가 수신될 때마다 실행될 이벤트 핸들러입니다.
@bot.event
async def on_message(message):
# 메시지 작성자가 봇 자신이면 아무 동작도 하지 않습니다. (무한 루프 방지)
if message.author == bot.user:
return
# 메시지 내용이 "안녕"이면 "안녕하세요!"라고 응답합니다.
if message.content == '안녕':
await message.channel.send('안녕하세요!')
# 다른 명령어도 처리할 수 있도록 RUF이벤트를 계속 처리하도록 전달합니다.
# 이 줄이 없으면 아래에 정의된 @bot.command() 명령어들이 작동하지 않습니다.
await bot.process_commands(message)
# 간단한 명령어 예시: !ping 이라고 입력하면 Pong! 이라고 응답합니다.
@bot.command(name='hello')
async def _say_hello(ctx):
"""/hello 명령어 입니다!"""
await ctx.send(f"안녕하세요, {ctx.author.name}님!")
# 봇을 실행합니다. 'YOUR_BOT_TOKEN' 부분에 발급받은 봇 토큰을 입력하세요.
# 중요: 절대로 토큰을 코드에 직접 하드코딩하지 마세요. 환경 변수 등을 사용하는 것이 안전합니다.
# 예시를 위해 직접 넣었지만, 실제 운영 시에는 os.getenv('DISCORD_TOKEN') 등을 사용하세요.
try:
bot.run('YOUR_BOT_TOKEN')
except discord.LoginFailure:
print("잘못된 토큰입니다. Discord Developer Portal에서 토큰을 확인하세요.")
except Exception as e:
print(f"봇 실행 중 오류 발생: {e}")
봇 테스트
- 위 코드를
main.py
와 같은 이름으로 저장해주세요. YOUR_BOR_TOKEN
이 부분을 실제 봇 토큰으로 변경해주세요.- 터미널에서
python main.py
를 실행합니다. - 콘솔에
봇이름이(가) 성공적으로 로그인했습니다!
메시지가 뜨는지 유심히 보세요. - 봇이 초대된 디스코드 서버 채널에서
/hello
명령을 입력하여 봇이 응답하는지 확인해보세요.
2. Cog 모듈화
봇의 기능이 많아지면 main.py
파일 하나에 모든 코드를 작성하기 어렵습니다!
이때 Cog
를 사용하여 기능별로 파일을 분리하면 코드를 체계적으로 관리할 수 있어요!
Cog는 discord.py
에서 모듈이나 익스텐션을 뜻하는 용어이다.
위에서 기본적인 디스코드 봇 구조에 대해서 간략하게 알아봤는데
Cog로 기능들을 모듈화하면 카테고리별로 묶어 관리하는 것이 가능해진다.
이를 통해 코드의 가독성이 향상되고 유지 관리가 용이해지는 것이다.
예시: '인사' 관련 기능을 greetings.py
파일로 분리해봅시다!
# cogs/greetings.py
import discord
from discord.ext import commands
# Cog 클래스를 정의합니다. commands.Cog를 상속받습니다.
class Greetings(commands.Cog):
# Cog 클래스의 생성자(__init__)에서 bot 객체를 인자로 받습니다.
def __init__(self, bot: commands.Bot):
self.bot = bot
self._last_member = None # 예시용 변수
# @commands.Cog.listener() 데코레이터를 사용하여 Cog 내부의 이벤트 리스너를 정의합니다.
@commands.Cog.listener()
async def on_member_join(self, member: discord.Member):
"""서버에 새로운 멤버가 들어왔을 때 실행됩니다."""
channel = member.guild.system_channel # 기본 시스템 채널 (설정되어 있어야 함)
if channel is not None:
await channel.send(f'환영합니다, {member.mention}님! {member.guild.name}에 오신 것을 환영해요!')
# @commands.command() 데코레이터를 사용하여 Cog 내부의 명령어를 정의합니다.
@commands.command(name='안녕')
async def _hello(self, ctx: commands.Context, *, member: discord.Member = None):
"""봇이 인사를 건넵니다."""
member = member or ctx.author # 대상이 지정되지 않으면 명령어를 사용한 사용자
if self._last_member is None or self._last_member.id != member.id:
await ctx.send(f'{member.name}님, 안녕하세요!')
else:
await ctx.send(f'{member.name}님, 또 오셨네요!')
self._last_member = member
# setup 함수는 Cog를 봇에 로드할 때 필수적입니다.
# 이 함수는 discord.py가 Cog를 로드할 때 자동으로 호출합니다.
async def setup(bot: commands.Bot):
await bot.add_cog(Greetings(bot)) # Greetings 클래스의 인스턴스를 생성하여 봇에 Cog로 추가합니다.
이제 main.py
파일을 수정하여 위 Cog
를 로드하도록 해야해요.
# main.py (수정된 버전)
import discord
from discord.ext import commands
import os # 운영체제 관련 기능을 사용하기 위해 import (토큰 관리 등)
import asyncio # 비동기 작업을 위해 import
# .env 파일 로드를 위한 라이브러리 (선택 사항, 환경 변수 관리에 유용)
# pip install python-dotenv
# from dotenv import load_dotenv
# load_dotenv()
# 인텐트 설정
intents = discord.Intents.default()
intents.message_content = True
intents.members = True # on_member_join 이벤트를 위해 members 인텐트 활성화
# 봇 객체 생성
bot = commands.Bot(command_prefix='!', intents=intents, help_command=None)
# 봇 준비 완료 시 실행될 이벤트
@bot.event
async def on_ready():
print(f'{bot.user.name}이(가) 성공적으로 로그인했습니다!')
print(f'봇 ID: {bot.user.id}')
print('------')
# 봇의 상태 메시지를 설정합니다.
await bot.change_presence(status=discord.Status.online, activity=discord.Game("!help 명령어 입력"))
# 초기 Cog 로드 함수
async def load_extensions():
# 'cogs' 폴더 안의 모든 .py 파일을 찾아 Cog로 로드합니다.
# 실제 사용 시에는 로드할 Cog 목록을 명시하는 것이 더 안전할 수 있습니다.
for filename in os.listdir('./cogs'):
if filename.endswith('.py'):
# 파일명에서 .py 확장자를 제거하여 모듈 이름으로 사용합니다. (예: cogs.greetings)
extension_name = f'cogs.{filename[:-3]}'
try:
await bot.load_extension(extension_name)
print(f'{extension_name} 로드 완료.')
except Exception as e:
print(f'{extension_name} 로드 실패: {e}')
# 비동기 메인 함수 정의 (봇 실행 및 Cog 로드를 위해)
async def main():
# 환경 변수에서 토큰을 가져옵니다. (권장 방식)
# token = os.getenv('DISCORD_TOKEN')
# if token is None:
# print("오류: DISCORD_TOKEN 환경 변수가 설정되지 않았습니다.")
# return
# 예시용 토큰 (실제 사용 시에는 위 환경 변수 방식을 사용하세요)
token = 'YOUR_BOT_TOKEN'
async with bot: # bot 객체를 컨텍스트 매니저로 사용하여 안전하게 시작 및 종료
await load_extensions() # Cog 로드
await bot.start(token) # 봇 시작
# 메인 함수 실행
if __name__ == '__main__':
# asyncio.run()은 Python 3.7 이상에서 사용 가능합니다.
try:
asyncio.run(main())
except KeyboardInterrupt:
print("봇 종료 중...")
except discord.LoginFailure:
print("잘못된 토큰입니다. Discord Developer Portal에서 토큰을 확인하세요.")
except Exception as e:
print(f"봇 실행 중 오류 발생: {e}")
@bot.command
데코레이터를 사용하는 일반 명령어는 @commands.command
로 대신 사용한다.
3. 더 다양한 기능 구현해보기
이제 슬래시 명령어, 임베드, 버튼 등 디스코드 봇을 더욱 풍부하게 만들어주는 기능들을 알아봅시다!
이 기능들은 대부분 Cog 내부에 구현하는 것을 추천합니다.
아래 예시들은 cogs/features.py
라는 파일에 작성한다고 가정할게요.
3-1. Slash Command (슬래시 명령어)
# cogs/features.py (일부)
import discord
from discord import app_commands # 슬래시 명령어를 위해 import
from discord.ext import commands
import random # 예시를 위해 random 모듈 import
class Features(commands.Cog):
def __init__(self, bot: commands.Bot):
self.bot = bot
# 슬래시 명령어 정의
# @app_commands.command() 데코레이터를 사용합니다.
# name: 명령어 이름 (슬래시 뒤에 입력할 부분)
# description: 명령어에 대한 설명
@app_commands.command(name="주사위", description="지정한 숫자 범위 내에서 랜덤한 숫자를 뽑습니다.")
# 파라미터 설명 추가 (선택 사항)
@app_commands.describe(최소값="주사위 눈의 최소값 (기본값: 1)", 최대값="주사위 눈의 최대값 (기본값: 6)")
async def _dice(self, interaction: discord.Interaction, 최소값: int = 1, 최대값: int = 6):
"""주사위를 굴립니다."""
if 최소값 >= 최대값:
await interaction.response.send_message("최소값은 최대값보다 작아야 합니다.", ephemeral=True) # ephemeral=True: 명령어 사용자에게만 보이는 응답
return
result = random.randint(최소값, 최대값)
await interaction.response.send_message(f"🎲 주사위를 굴려 {result}이(가) 나왔습니다! ({최소값}-{최대값})")
# 슬래시 명령어 동기화 (중요!)
# 일반적으로 봇이 준비되었을 때 한 번 호출하거나, 별도의 동기화 명령어를 만듭니다.
# 너무 자주 호출하면 Rate Limit에 걸릴 수 있습니다.
@commands.Cog.listener()
async def on_ready(self):
try:
# 현재 길드(서버)에만 명령어 동기화 (개발 시 유용)
# guild = discord.Object(id=YOUR_GUILD_ID) # YOUR_GUILD_ID를 실제 서버 ID로 교체
# await self.bot.tree.sync(guild=guild)
# 모든 길드에 명령어 동기화 (전역 명령어, 반영에 시간이 걸릴 수 있음)
synced = await self.bot.tree.sync()
print(f"{len(synced)}개의 슬래시 명령어를 동기화했습니다.")
except Exception as e:
print(f"명령어 트리 동기화 실패: {e}")
# setup 함수 (Cog 로딩 시 필요)
async def setup(bot: commands.Bot):
await bot.add_cog(Features(bot))
# 슬래시 명령어는 Cog 로드 후 동기화가 필요할 수 있습니다.
# on_ready에서 처리하거나, 여기서 bot.tree.sync()를 호출할 수도 있지만 on_ready가 더 안정적입니다.
주요 포인트
discord.Interaction
: 슬래시 명령어 함수의 첫 번째 인자는 항상Interaction
객체입니다. 이 객체를 통해 응답(interaction.response.send_message
) 등을 처리합니다.app_commands.describe
: 명령어의 파라미터에 대한 설명을 추가하여 사용자 편의성을 높입니다.interaction.response.send_message()
: 슬래시 명령어에 대한 응답입니다.ephemeral=True
옵션은 해당 명령어를 사용한 유저에게만 보이도록 합니다.bot.tree.sync()
: 작성된 슬래시 명령어를 디스코드 서버에 등록(동기화)하는 과정입니다. 봇이 켜질 때on_ready
이벤트에서 호출하거나, 관리자용 동기화 명령어를 만들어 필요할 때만 실행하는 것이 좋습니다. 전역 동기화는 반영되기까지 최대 1시간이 소요될 수 있습니다. 특정 서버에만 동기화(guild=discord.Object(id=...)
)하면 즉시 반영됩니다.
만약 특정 채널에서만 명령어가 실행 가능하도록 하려면 다음과 같이 코드를 작성하면 됩니다.
import discord
from discord.ext import commands
from discord import app_commands
class SlashCommand(commands.Cog):
def __init__(self, bot: commands.Bot) -> None:
super().__init__()
self.bot = bot
self.channel_id: int = 000000 # 명령어 사용이 가능한 채널의 채널 ID 입력
@commands.Cog.listener()
async def on_ready(self) -> None:
print(f"{self.__class__.__name__} Cog is ready.")
@app_commands.command(
name="sayhello", description="사용자에게 인사를 건네는 명령어입니다!"
)
async def say_hello(self, interaction: discord.Interaction):
# 특정 채널인지 확인
if interaction.channel.id != self.channel_id:
await interaction.response.send_message(f"이 명령어는 <#{self.channel_id}> 채널에서만 사용 가능합니다!")
return
await interaction.response.send_message(f"Hello, {interaction.user.name}!")
async def setup(bot: commands.Bot):
await bot.add_cog(SlashCommand(bot=bot))
또한 어드민 권한이 있는 유저만 /sayhello
명령어 사용이 가능하도록 변경하려면 다음과 같이 코드를 작성해주세요.
import discord
from discord.ext import commands
from discord import app_commands
class SlashCommand(commands.Cog):
def __init__(self, bot: commands.Bot) -> None:
super().__init__()
self.bot = bot
@commands.Cog.listener()
async def on_ready(self) -> None:
print(f"{self.__class__.__name__} Cog is ready.")
@app_commands.command(
name="sayhello", description="사용자에게 인사를 건네는 명령어입니다!"
)
async def say_hello(self, interaction: discord.Interaction):
# 어드민 권한 확인
if not interaction.user.guild_permissions.administrator:
await interaction.response.send_message(f"{interaction.user.name} 님은 이 명령어를 사용할 권한이 없습니다!")
return
await interaction.response.send_message(f"Hello, {interaction.user.name}!")
async def setup(bot: commands.Bot):
await bot.add_cog(SlashCommand(bot=bot))
마지막으로 명령어에 인자를 받아서 처리하고 싶다면 @app_commands.command
에 인자를 정의해주면 됩니다.
예를 들어, 사용자가 /명령어 <내용>
과 같은 명령어 사용이 가능하게끔 해줘요.
import discord
from discord.ext import commands
from discord import app_commands
class SlashCommand(commands.Cog):
def __init__(self, bot: commands.Bot) -> None:
super().__init__()
self.bot = bot
@commands.Cog.listener()
async def on_ready(self) -> None:
print(f"{self.__class__.__name__} Cog is ready.")
@app_commands.command(
name="sayhello", description="사용자에게 인사를 건네는 명령어입니다!"
)
@app_commands.describe(content="공지할 점검 내용을 입력하세요.")
async def say_hello(self, interaction: discord.Interaction, content: str):
# 어드민 권한 확인
if not interaction.user.guild_permissions.administrator:
await interaction.response.send_message(f"{interaction.user.name} 님은 이 명령어를 사용할 권한이 없습니다!")
return
await interaction.response.send_message(f"Hello, {interaction.user.name}!\nyour content is: {content}")
async def setup(bot: commands.Bot):
await bot.add_cog(SlashCommand(bot=bot))
주의할 점으로는 @app_commands.describe
데코레이터에서 받는 content
매개 변수와 say_hello
메소드에서 받는 content
매개 변수의 이름이 동일해야 해요!
3-1-1. 테스트
코드를 실행하고 디스코드로 이동하여 CTRL
+ R
을 클릭해 디스코드를 새로고침 해주자.
명령어가 정상적으로 등록된 것이 보인다!
3-2. Embed 메시지 만들기
Embed는 텍스트만으로는 표현하기 어려운 정보를 시각적으로 보기 좋게 전달할 수 있는 강력한 기능이에요!
제목, 설명, 필드, 이미지, 색상 등을 설정할 수 있습니다.
# cogs/features.py (Embed 예제 추가)
import discord
from discord import app_commands
from discord.ext import commands
import datetime # timestamp를 위해 import
class Features(commands.Cog):
# ... (이전 __init__, 주사위 명령어 등) ...
@app_commands.command(name="정보", description="봇 또는 서버 정보를 임베드로 보여줍니다.")
@app_commands.describe(대상="정보를 볼 대상을 선택하세요.")
@app_commands.choices(대상=[ # 선택지 제공
app_commands.Choice(name="봇", value="bot"),
app_commands.Choice(name="서버", value="server"),
])
async def _info(self, interaction: discord.Interaction, 대상: app_commands.Choice[str]):
"""봇 또는 서버 정보를 보여주는 임베드 메시지를 전송합니다."""
if 대상.value == "bot":
embed = discord.Embed(
title="🤖 봇 정보",
description=f"{self.bot.user.name}의 정보입니다.",
color=discord.Color.blue(), # Embed의 왼쪽 색상 막대 색깔
timestamp=datetime.datetime.now(datetime.timezone.utc) # Embed 생성 시간 기록
)
embed.set_author(name=self.bot.user.name, icon_url=self.bot.user.display_avatar.url)
embed.add_field(name="봇 이름", value=self.bot.user.name, inline=True)
embed.add_field(name="봇 ID", value=self.bot.user.id, inline=True)
embed.add_field(name="소속 서버 수", value=f"{len(self.bot.guilds)}개", inline=False)
# 봇 개발자 정보 (예시 - 실제로는 봇 객체에서 직접 가져오기 어려울 수 있음)
# app_info = await self.bot.application_info()
# embed.add_field(name="개발자", value=app_info.owner.name, inline=True)
embed.set_thumbnail(url=self.bot.user.display_avatar.url) # 작은 이미지 (썸네일)
embed.set_footer(text=f"요청자: {interaction.user.display_name}", icon_url=interaction.user.display_avatar.url)
elif 대상.value == "server":
guild = interaction.guild # 명령어가 사용된 서버 정보
if guild is None:
await interaction.response.send_message("서버 정보를 DM에서는 볼 수 없습니다.", ephemeral=True)
return
embed = discord.Embed(
title="🏰 서버 정보",
description=f"{guild.name} 서버의 정보입니다.",
color=discord.Color.green(),
timestamp=datetime.datetime.now(datetime.timezone.utc)
)
if guild.icon:
embed.set_thumbnail(url=guild.icon.url)
embed.add_field(name="서버 이름", value=guild.name, inline=True)
embed.add_field(name="서버 ID", value=guild.id, inline=True)
embed.add_field(name="서버 주인", value=guild.owner.mention, inline=False) # 주인을 멘션
embed.add_field(name="멤버 수", value=f"{guild.member_count}명", inline=True)
embed.add_field(name="생성일", value=guild.created_at.strftime("%Y년 %m월 %d일"), inline=True) # 날짜 형식 지정
# embed.set_image(url=guild.banner.url if guild.banner else None) # 큰 이미지 (배너 등)
embed.set_footer(text=f"요청자: {interaction.user.display_name}", icon_url=interaction.user.display_avatar.url)
else:
await interaction.response.send_message("알 수 없는 대상입니다.", ephemeral=True)
return
await interaction.response.send_message(embed=embed)
# setup 함수는 파일 맨 아래에 한 번만 있으면 됩니다.
# async def setup(bot: commands.Bot):
# await bot.add_cog(Features(bot))
3-2-1. 테스트
코드를 실행하고 디스코드로 이동하여 CTRL
+ R
을 클릭해 디스코드를 새로고침 해주자.
3-3. 반복 작업 처리: task
정해진 시간마다 특정 작업을 수행해야 할 때 (예: 매시간 공지, 주기적인 데이터 확인 등)
discord.ext.tasks
를 사용해보세요!
# cogs/tasks_example.py (새로운 Cog 파일 예시)
import discord
from discord.ext import commands, tasks
import datetime
class TasksExample(commands.Cog):
def __init__(self, bot: commands.Bot):
self.bot = bot
# Cog가 로드될 때 루프를 시작합니다.
self.my_background_task.start()
def cog_unload(self):
# Cog가 언로드될 때 루프를 중지합니다. (봇 종료 시 등)
self.my_background_task.cancel()
# tasks.loop 데코레이터를 사용하여 반복 작업을 정의합니다.
# seconds, minutes, hours 중 하나 또는 조합하여 실행 주기를 설정합니다.
@tasks.loop(minutes=30) # 예시: 30분마다 실행
async def my_background_task(self):
# 특정 채널에 메시지 보내기 (채널 ID를 알아야 함)
channel_id = 123456789012345678 # 실제 알림을 보낼 채널 ID로 변경하세요.
channel = self.bot.get_channel(channel_id)
if channel:
now = datetime.datetime.now().strftime("%Y-%m-%d %H:%M:%S")
await channel.send(f"🕒 현재 시각: {now}. 정기 알림입니다!")
else:
print(f"오류: 채널 ID {channel_id}를 찾을 수 없습니다.")
# 루프가 처음 시작되기 전에 실행될 작업을 정의할 수 있습니다.
@my_background_task.before_loop
async def before_my_task(self):
# 봇이 완전히 준비될 때까지 기다립니다.
# Cog 로딩 시점에 봇이 아직 준비되지 않았을 수 있기 때문입니다.
await self.bot.wait_until_ready()
print("백그라운드 작업 루프 시작 준비 완료.")
# setup 함수
async def setup(bot: commands.Bot):
await bot.add_cog(TasksExample(bot))
3-3-1. 테스트
코드를 실행하고 디스코드로 이동하여 CTRL
+ R
을 클릭해 디스코드를 새로고침 해주자.
3-4. 봇 상태 처리: Activity Type
봇의 프로필 아래에 표시되는 상태 메시지(~하는 중
, ~을 듣는중
등)를 변경할 수 있어요.
# main.py 또는 특정 Cog의 on_ready 등에서 설정 가능
# ... (import discord 등) ...
@bot.event
async def on_ready():
print(f'{bot.user.name}이(가) 성공적으로 로그인했습니다!')
# ...
# 봇 상태 설정 예시
# activity = discord.Game(name="!help 명령어") # ~하는 중
# activity = discord.Streaming(name="개발 방송", url="https://twitch.tv/...") # 방송 중
# activity = discord.Listening(name="음악") # ~듣는 중
activity = discord.Activity(type=discord.ActivityType.watching, name="서버 활동") # ~보는 중
# activity = discord.Activity(type=discord.ActivityType.competing, name="경쟁") # 경쟁 중 (최신 버전)
await bot.change_presence(status=discord.Status.online, activity=activity)
# status: online, idle, dnd (do not disturb), invisible
print("봇 상태 메시지 설정 완료.")
# ... (Cog 로드 등)
3-4-1. 테스트
코드를 실행하고 디스코드로 이동하여 CTRL
+ R
을 클릭해 디스코드를 새로고침 해주자.
3-5. 생성형 버튼: 유저 상호작용 강화하기
사용자가 클릭하여 상효작용할 수 있는 버튼을 메시지에 추가할 수 있어요!
discord.ui
모듈을 사용합니다.
# cogs/features.py (버튼 예제 추가)
import discord
from discord import app_commands, ui # ui 모듈 추가
from discord.ext import commands
# 버튼을 포함할 View 클래스를 정의합니다. discord.ui.View를 상속받습니다.
class SimpleView(ui.View):
def __init__(self, *, timeout=180): # timeout: 버튼이 활성화되어 있는 시간 (초), 기본값 180초
super().__init__(timeout=timeout)
self.click_count = 0
# @ui.button() 데코레이터를 사용하여 버튼을 정의합니다.
@ui.button(label="클릭하세요!", style=discord.ButtonStyle.primary, emoji="🖱️")
async def hello_button(self, interaction: discord.Interaction, button: ui.Button):
"""사용자가 버튼을 클릭했을 때 실행될 콜백 함수입니다."""
self.click_count += 1
# 버튼 스타일 변경 예시 (클릭 시 회색으로)
button.style = discord.ButtonStyle.secondary
button.disabled = True # 버튼 비활성화
button.label = "클릭됨"
# 응답 메시지 (ephemeral=True 로 설정하면 클릭한 사용자에게만 보임)
await interaction.response.send_message(f"버튼이 {self.click_count}번 클릭되었습니다!", ephemeral=True)
# 원래 메시지를 수정하여 View를 업데이트 (버튼 상태 변경 등을 반영)
await interaction.message.edit(view=self)
@ui.button(label="취소", style=discord.ButtonStyle.danger)
async def cancel_button(self, interaction: discord.Interaction, button: ui.Button):
"""취소 버튼 클릭 시 실행될 콜백 함수입니다."""
# View의 모든 버튼 비활성화
for item in self.children:
if isinstance(item, ui.Button):
item.disabled = True
await interaction.response.send_message("작업이 취소되었습니다.", ephemeral=True)
# 메시지 수정하여 비활성화된 버튼 반영
await interaction.message.edit(content="버튼 상호작용이 종료되었습니다.", view=self)
# View 자체를 중지 (더 이상 상호작용 불가)
self.stop()
class Features(commands.Cog):
# ... (이전 코드) ...
@app_commands.command(name="버튼", description="간단한 버튼 예제를 보여줍니다.")
async def _button_test(self, interaction: discord.Interaction):
"""버튼이 포함된 메시지를 전송합니다."""
view = SimpleView()
await interaction.response.send_message("아래 버튼을 클릭해보세요:", view=view)
# View의 타임아웃 처리 (선택 사항)
await view.wait() # View가 멈출 때까지 (timeout 또는 self.stop() 호출 시) 기다립니다.
if view.timeout: # 타임아웃으로 인해 멈췄는지 확인
# 타임아웃 시 메시지 수정 (예: 버튼 비활성화)
for item in view.children:
if isinstance(item, ui.Button):
item.disabled = True
# followup.edit_message 를 사용해야 함 (이미 응답했으므로)
await interaction.followup.edit_message(interaction.message.id, content="버튼 시간이 만료되었습니다.", view=view)
# setup 함수
# async def setup(bot: commands.Bot):
# await bot.add_cog(Features(bot))
3-5-1. 테스트
코드를 실행하고 디스코드로 이동하여 CTRL
+ R
을 클릭해 디스코드를 새로고침 해주자.
3-6. 사용자 입력받기: Form 생성 (Modal)
버튼보다 더 복잡한 사용자 입력을 받아야 할 때 (예: 여러 필드로 구성된 신청서 등) Modal(팝업 창)을 사용합니다.
이 역시 discord.ui
를 사용해요.
# cogs/features.py (Modal 예제 추가)
import discord
from discord import app_commands, ui
from discord.ext import commands
# 사용자 입력을 받을 Modal 클래스를 정의합니다. discord.ui.Modal을 상속받습니다.
class FeedbackModal(ui.Modal, title="피드백 제출"):
# ui.TextInput을 사용하여 입력 필드를 정의합니다.
# label: 필드 이름
# style: discord.TextStyle.short (한 줄 입력), discord.TextStyle.paragraph (여러 줄 입력)
# placeholder: 입력 예시
# required: 필수 입력 여부 (True/False)
# max_length: 최대 입력 글자 수
subject = ui.TextInput(label="제목", style=discord.TextStyle.short, placeholder="피드백 주제를 입력하세요.", required=True, max_length=100)
message = ui.TextInput(label="내용", style=discord.TextStyle.paragraph, placeholder="자세한 내용을 입력하세요.", required=True, max_length=1000)
# 사용자가 '제출' 버튼을 눌렀을 때 실행될 콜백 함수입니다.
async def on_submit(self, interaction: discord.Interaction):
"""Modal 제출 시 실행됩니다."""
# 입력된 내용 확인
subject_text = self.subject.value
message_text = self.message.value
# 예시: 특정 채널에 Embed로 피드백 내용 전송
feedback_channel_id = 123456789012345678 # 피드백 받을 채널 ID로 변경
channel = interaction.guild.get_channel(feedback_channel_id)
if channel:
embed = discord.Embed(title="📝 새 피드백 도착", color=discord.Color.gold())
embed.set_author(name=interaction.user.display_name, icon_url=interaction.user.display_avatar.url)
embed.add_field(name="제목", value=subject_text, inline=False)
embed.add_field(name="내용", value=message_text, inline=False)
embed.timestamp = datetime.datetime.now(datetime.timezone.utc)
await channel.send(embed=embed)
await interaction.response.send_message("피드백이 성공적으로 제출되었습니다. 감사합니다!", ephemeral=True)
else:
await interaction.response.send_message("오류: 피드백 채널을 찾을 수 없습니다.", ephemeral=True)
# 사용자가 Modal을 닫거나 오류가 발생했을 때 실행될 콜백 함수입니다. (선택 사항)
async def on_error(self, interaction: discord.Interaction, error: Exception) -> None:
await interaction.response.send_message(f"오류가 발생했습니다: {error}", ephemeral=True)
# 에러 로깅 등 추가 작업 가능
class Features(commands.Cog):
# ... (이전 코드) ...
@app_commands.command(name="피드백", description="봇 개선을 위한 피드백을 제출합니다.")
async def _feedback(self, interaction: discord.Interaction):
"""피드백 Modal을 엽니다."""
# interaction.response.send_modal()을 사용하여 Modal을 띄웁니다.
await interaction.response.send_modal(FeedbackModal())
# setup 함수
# async def setup(bot: commands.Bot):
# await bot.add_cog(Features(bot))
3-6-1. 테스트
코드를 실행하고 디스코드로 이동하여 CTRL
+ R
을 클릭해 디스코드를 새로고침 해주자.
3-7. 자동 삭제 메시지 (delete_after)
봇이 보낸 메시지가 일정 시간 후에 자동으로 삭제되도록 할 수 있어요.
명령어 결과 안내, 임시 알림 등에 유용합니다.
# cogs/features.py (자동 삭제 예제 추가)
import discord
from discord import app_commands
from discord.ext import commands
import asyncio # sleep을 위해 import (예시용)
class Features(commands.Cog):
# ... (이전 코드) ...
@app_commands.command(name="임시공지", description="메시지를 보낸 후 10초 뒤에 자동으로 삭제합니다.")
@app_commands.describe(내용="공지할 내용을 입력하세요.")
async def _temp_notice(self, interaction: discord.Interaction, 내용: str):
"""자동 삭제 메시지를 보냅니다."""
# send_message 함수의 delete_after 파라미터에 삭제까지 걸리는 시간(초)을 지정합니다.
await interaction.response.send_message(f"📢 임시 공지: {내용}\n(이 메시지는 10초 후 사라집니다.)", delete_after=10.0)
# Prefix 명령어 예시 (참고용)
@commands.command(name='임시')
async def _temp_prefix(self, ctx: commands.Context, *, message: str):
"""Prefix 명령어로 자동 삭제 메시지 보내기"""
await ctx.send(f"임시 메시지: {message} (5초 후 삭제)", delete_after=5.0)
# 명령어 메시지도 삭제하고 싶다면 (봇에게 메시지 관리 권한 필요)
await asyncio.sleep(5.0) # 메시지가 삭제될 때까지 기다린 후
try:
await ctx.message.delete() # 명령어 메시지 삭제 시도
except discord.Forbidden:
print("명령어 메시지를 삭제할 권한이 없습니다.")
except discord.NotFound:
pass # 이미 삭제되었거나 찾을 수 없는 경우 무시
# setup 함수
# async def setup(bot: commands.Bot):
# await bot.add_cog(Features(bot))
3-8. 채널 메시지 삭제하기
특정 채널의 메시지를 대량으로 삭제하는 기능입니다.
봇에게 "메시지 관리" 기능이 필요해요.
# cogs/moderation.py (새로운 Cog 파일 예시 - 관리 기능)
import discord
from discord import app_commands
from discord.ext import commands
class Moderation(commands.Cog):
def __init__(self, bot: commands.Bot):
self.bot = bot
@app_commands.command(name="청소", description="지정한 개수만큼 현재 채널의 메시지를 삭제합니다.")
@app_commands.describe(개수="삭제할 메시지의 개수 (최대 100개)")
# @app_commands.checks.has_permissions(manage_messages=True) # 슬래시 명령어 권한 체크 (봇과 사용자 모두 권한 필요)
async def _purge(self, interaction: discord.Interaction, 개수: app_commands.Range[int, 1, 100]): # Range로 입력값 제한
"""채널 메시지를 삭제합니다."""
# 명령어를 사용한 사용자에게 메시지 관리 권한이 있는지 확인
if not interaction.user.guild_permissions.manage_messages:
await interaction.response.send_message("메시지를 삭제할 권한이 없습니다.", ephemeral=True)
return
# 봇에게 메시지 관리 권한이 있는지 확인 (Interaction에서는 직접 확인 어려움, 역할 권한으로 관리)
# if not interaction.app_permissions.manage_messages:
# await interaction.response.send_message("봇에게 메시지 관리 권한이 없습니다.", ephemeral=True)
# return
channel = interaction.channel # 명령어가 실행된 채널
try:
# channel.purge()를 사용하여 메시지를 삭제합니다. limit으로 개수 지정.
# bulk=True 가 기본값이며, 2주 이상 지난 메시지는 개별 삭제 시도 (느릴 수 있음)
deleted = await channel.purge(limit=개수)
# ephemeral=True 로 설정하면 명령어 사용자에게만 확인 메시지가 보입니다.
await interaction.response.send_message(f"{len(deleted)}개의 메시지를 성공적으로 삭제했습니다.", ephemeral=True, delete_after=5.0)
except discord.Forbidden:
await interaction.response.send_message("오류: 봇에게 메시지를 삭제할 권한이 없습니다.", ephemeral=True)
except discord.HTTPException as e:
await interaction.response.send_message(f"메시지 삭제 중 오류가 발생했습니다: {e}", ephemeral=True)
# Prefix 명령어 버전 (참고용)
@commands.command(name='삭제')
@commands.has_permissions(manage_messages=True) # 명령어 사용자 권한 체크
@commands.bot_has_permissions(manage_messages=True) # 봇 권한 체크
async def _purge_prefix(self, ctx: commands.Context, amount: int = 5):
"""Prefix 명령어로 메시지 삭제 (!삭제 [개수])"""
if amount > 100:
await ctx.send("한 번에 최대 100개까지만 삭제할 수 있습니다.", delete_after=5.0)
return
try:
deleted = await ctx.channel.purge(limit=amount + 1) # +1 하는 이유: 명령어 메시지 자체도 포함하기 위해
await ctx.send(f"{len(deleted) - 1}개의 메시지를 삭제했습니다.", delete_after=5.0) # 확인 메시지는 남김
except discord.Forbidden:
await ctx.send("오류: 봇에게 메시지 삭제 권한이 없습니다.", delete_after=5.0)
except discord.HTTPException as e:
await ctx.send(f"메시지 삭제 중 오류가 발생했습니다: {e}", delete_after=5.0)
# Prefix 명령어 오류 처리 예시
@_purge_prefix.error
async def purge_error(self, ctx, error):
if isinstance(error, commands.MissingPermissions):
await ctx.send("메시지를 삭제할 권한이 없습니다.", delete_after=5.0)
elif isinstance(error, commands.BotMissingPermissions):
await ctx.send("봇에게 메시지 삭제 권한이 없습니다.", delete_after=5.0)
elif isinstance(error, commands.MissingRequiredArgument):
await ctx.send("삭제할 메시지 개수를 입력해주세요. (예: !삭제 10)", delete_after=5.0)
elif isinstance(error, commands.BadArgument):
await ctx.send("삭제할 메시지 개수는 숫자로 입력해주세요.", delete_after=5.0)
else:
print(f"삭제 명령어 오류: {error}")
await ctx.send("명령어 실행 중 오류가 발생했습니다.", delete_after=5.0)
# setup 함수
async def setup(bot: commands.Bot):
await bot.add_cog(Moderation(bot))
3-9. 응답 지연 해결하기 - Interaction.response.defer()
슬래시 명령어 또는 버튼/Modal 콜백 등 Interaction을 처리할 때,
봇이 응답하기까지 3초 이상 소요되면 디스코드는 "상호작용 실패" 오류를 발생시킵니다.
복잡한 계산, 외부 API 호출 등으로 응답이 늦어질 경우
interaction.response.defer()
를 사용하여 응답 시간을 연장해야 해요.
# cogs/features.py (defer 예제 추가)
import discord
from discord import app_commands
from discord.ext import commands
import asyncio # 시간 지연을 위해 import
class Features(commands.Cog):
# ... (이전 코드) ...
@app_commands.command(name="느린작업", description="시간이 오래 걸리는 작업을 시뮬레이션합니다.")
async def _slow_task(self, interaction: discord.Interaction):
"""응답 지연 처리를 위한 defer() 사용 예제"""
# defer()를 호출하여 디스코드에게 '처리 중'임을 알립니다.
# ephemeral=True로 설정하면 '봇이 생각 중...' 메시지가 사용자에게만 보입니다.
await interaction.response.defer(ephemeral=False, thinking=True) # thinking=True: '봇이 생각 중...' 표시
# 시간이 오래 걸리는 작업 수행 (예: 5초 대기)
await asyncio.sleep(5)
# defer() 호출 후에는 interaction.response.send_message() 대신
# interaction.followup.send() 또는 interaction.followup.edit_message()를 사용해야 합니다.
await interaction.followup.send(f"✅ {interaction.user.mention}, 오래 기다리셨죠! 작업이 완료되었습니다.")
# 또는 기존 '생각 중...' 메시지를 수정하려면:
# await interaction.followup.edit_message(interaction.message.id, content=f"✅ {interaction.user.mention}, 작업 완료!")
# setup 함수
# async def setup(bot: commands.Bot):
# await bot.add_cog(Features(bot))
작동 방식
- 사용자가
/느린작업
명령어를 실행합니다. - 봇은 즉시
interaction.response.defer()
를 호출합니다. - 디스코드는 사용자에게 "봇이 생각 중입니다..." (또는
ephemeral=True
시 사용자에게만 보임) 메시지를 표시하고, 응답 대기 시간을 15분으로 연장합니다. - 봇은 5초 동안
asyncio.sleep(5)
를 실행합니다 (실제로는 오래 걸리는 작업). - 작업 완료 후,
interaction.followup.send()
를 사용하여 최종 응답 메시지를 보냅니다. defer()
를 사용하지 않으면?asyncio.sleep(5)
실행 후interaction.response.send_message()
를 호출하려고 할 때, 이미 3초가 지났으므로 "상호작용 실패(Interaction failed)" 오류가 발생합니다.
4. 그 외 자주 쓰이는 기능들
위 소개된 기능 외에도 디스코드 봇 개발에 유용한 몇 가지 개념과 기능을 간략하게 알아볼게요.
오류 처리 (Error Handling)
- Cog 별 오류 핸들러: Cog 클래스 내에
async def cog_command_error(self, ctx, error):
또는async def cog_app_command_error(self, interaction, error):
메서드를 정의하여 해당 Cog에서 발생하는 명령어 오류를 처리할 수 있습니다. - 전역 오류 핸들러: main.py 등에서
@bot.event async def on_command_error(ctx, error):
또는@bot.event async def on_app_command_error(interaction, error):
를 정의하여 처리되지 않은 모든 명령어 오류를 잡을 수 있습니다.
try...except 구문: 개별 명령어 함수 내에서 특정 오류를 직접 처리할 수 있습니다.
- 주요 오류 유형:
commands.CommandNotFound
,commands.MissingRequiredArgument
,commands.BadArgument
,commands.CheckFailure
(권한 부족 등),commands.CommandOnCooldown
등 다양한 오류 타입에 따라 적절한 사용자 피드백을 제공하는 것이 중요합니다.
권한 확인 (Permission Checks)
- 데코레이터 사용:
@commands.has_permissions(administrator=True)
(Prefix 명령어),@app_commands.checks.has_permissions(manage_messages=True)
(Slash 명령어) 등을 사용하여 명령어 실행 전에 사용자 또는 봇의 권한을 자동으로 확인합니다. - 수동 확인:
interaction.user.guild_permissions.manage_guild
또는ctx.author.guild_permissions.kick_members
와 같이guild_permissions
속성을 통해 특정 권한 보유 여부를 True/False로 확인할 수 있습니다.interaction.app_permissions
는 슬래시 명령어에서 봇 자신의 권한을 확인합니다.
객체 가져오기 (Getting Objects)
- ID를 사용하여 특정 서버(Guild), 채널(Channel), 사용자(User/Member), 역할(Role) 등의 객체를 가져올 수 있습니다.
guild = bot.get_guild(guild_id)
channel = bot.get_channel(channel_id)
또는guild.get_channel(channel_id)
member = guild.get_member(user_id)
role = guild.get_role(role_id)
user = await bot.fetch_user(user_id)
(봇과 해당 유저가 같은 서버에 없어도 정보 가져오기 시도)message = await channel.fetch_message(message_id)
이벤트 핸들링 (Event Handling)
on_ready
,on_message
외에도 다양한 이벤트에 반응하는 코드를 작성할 수 있습니다.on_member_join(member)
: 멤버가 서버에 참여했을 때on_member_remove(member)
: 멤버가 서버를 떠났을 때on_reaction_add(reaction, user)
: 메시지에 반응이 추가되었을 때on_voice_state_update(member, before, after)
: 멤버의 음성 채널 상태 변경 시 (음성 기능 구현 시 중요)- 전체 이벤트 목록은 discord.py 문서에서 확인할 수 있습니다.
매개변수 타입힌트 (TypeHint) 지정하기
코드를 작성하다보면 매개변수들의 타입힌트(Typehint)을 지정하지 않아서 자동완성이 되지 않는 불편한 상황을 겪을 수 있다.
이럴 때에는 내가 궁금한 매개변수들의 타입을 type()
함수로 출력하여 어떤 타입이 반환되는지 확인하고
해당 반환 타입을 매개변수의 타입힌트로 지정해주면 된다.
그러면 Cog
클래스에서 ctx
매개변수의 타입이 궁금하다면 어떻게 해야할까?
cogs/general.py
Cog를 기준으로 설명해보겠다.
import discord
from discord.ext import commands
class General(commands.Cog):
def __init__(self, bot) -> None:
self.bot = bot
# ctx 매개변수 타입 확인을 위해 /hello 입력 시 타입이 출력되도록 코드 수정
@commands.command()
async def hello(self, ctx) -> None:
print(f"ctx -> {type(ctx)})
# echo 명령어
@commands.command()
async def echo(self, ctx, *, content: str) -> None:
await ctx.send(content)
# Cog를 등록하는 함수
async def setup(bot) -> None:
await bot.add_cog(General(bot))
이렇게하면 /hello
입력 시 매개변수 ctx
에 대한 타입이 출력될 것이다.
출력해보니 다음과 같은 타입이 출력된다.
봇이 실행되었음: DevBot#0737
General is going ...
<discord.ext.commands.context.Context object at 0x000001D175995040> # 출력된 ctx 매개변수의 타입
이를 기반으로 매개변수에 다음과 같이 타입힌트를 지정해주면 된다.
import discord
from discord.ext import commands
class General(commands.Cog):
...
@commands.command()
async def hello(self, ctx: commands.Context) -> None:
ctx.send(f"안녕하세요, {ctx.author.name}님!")
...
채널 ID 확인 방법
Discord 채널 ID는 디스코드 서버 각 채널에 할당되는 고유 식별값이다.
Discord를 이용하는데에는 채널 ID를 몰라도 문제될 게 없지만,
다양한 API 사용이나 명령, Bot 활용 등을 위해서는 필요할 수 있다.
해당 챕터에서는 디스코드에서 채널 ID를 찾는 방법에 대해서 간단하게 설명해보겠다.
1. 개발자 모드 활성화
먼저 개발자 모드가 활성화되어 있는지 확인이 필요하다. 사용자 설정을 클릭해보자.
비활성화 상태라면 고급 -> 개발자 모드를 클릭하여 활성화해주자.
2. 채널 ID 복사
다시 서버로 돌아와서 원하는 채널을 마우스 우클릭 후 메뉴에서 "ID 복사하기"를 클릭하자. 이제 필요한 곳에 붙여넣기를 하면된다.